#include "depui/depui.h"
#include <string.h>

/**todo Updating of the screen after moving of the cursor could be made much
more efficient.  Right now the object is always updated, we only have to update
the previous and current cursor positions */

/**todo MX_EDITTEXT needs pageup and page down keys */

void mx__edittext_cursorpos(MX_EDITTEXT_DATA * edit, int newpos)
{
	int coff, len = 0;
	MX__TEXTCHUNK *chunk, *ptr;
	MX_TEXTUAL_DATA *textual = MXTEXTUAL(edit);
	void *ffont = mx__textual_platform_font(textual);
	int spacew = mx_platform_font_width(ffont, " ", -1);
	const char *text;
	char cursortext[2] = " ";

	edit->_cursor = newpos;
	if (edit->_cursor < 0) {
		if (edit->_cursorchunk)
			mx__textchunk_free(edit->_cursorchunk);
		edit->_cursorchunk = 0;
		edit->_cursorpointedchunk = 0;
		edit->_pixeloffset = 0;
		return;
	}

	edit->_cursor = MX_MIN(edit->_cursor, (int) mx_vector_size(&edit->_text));

	ptr = mx_dllist_first(textual);
	chunk = ptr;

	while (ptr) {
		if (ptr->offset > edit->_cursor)
			break;
		if (ptr->normal)
			chunk = ptr;
		ptr = mx_dllist_next(ptr);
	}

	assert(chunk);
	edit->_cursorpointedchunk = chunk;
	assert(chunk->normal);
	assert(chunk->offset <= edit->_cursor);

	coff = edit->_cursor - chunk->offset;
	text = mx_string_text(&chunk->text, &len);
	if (coff < len) {
		edit->_pixeloffset = chunk->rect.x1 + mx_platform_font_width(ffont, text, coff);
		spacew = mx_platform_font_width(ffont, &text[coff], 1);
		spacew = MX_MAX(spacew, 2);
		cursortext[0] = text[coff];
	} else
		edit->_pixeloffset = chunk->rect.x2;

	if (!edit->_cursorchunk)
		edit->_cursorchunk = mx__textchunk(textual);
	assert(edit->_cursorchunk);

	mx_string_set(&edit->_cursorchunk->text, cursortext, -1, 0);
	mx_string_realloc(&edit->_cursorchunk->text, mx_malloc, mx_free);
	edit->_cursorchunk->offset = edit->_cursor;
	edit->_cursorchunk->rect.x1 = edit->_pixeloffset;
	edit->_cursorchunk->rect.y1 = chunk->rect.y1;
	edit->_cursorchunk->rect.x2 = edit->_pixeloffset + spacew;
	edit->_cursorchunk->rect.y2 = chunk->rect.y2;
	edit->_cursorchunk->normal = false;
	edit->_cursorchunk->cursor = true;
}

static void mx__edittext_chunker(MX_TEXTUAL_DATA * textual)
{
	MX_EDITTEXT_DATA *edit = (MX_EDITTEXT_DATA *) textual;
	void *ffont = mx__textual_platform_font(textual);

	MXDEBUG_INVARIANT(MXOBJ(edit));

	mx_vector_extend(&edit->_text, 10);

	mx___textual_chunker(textual);
	edit->_cursorchunk = 0;
	edit->_cursorpointedchunk = 0;

	/* Make room for cursor at the end of the longest line */
	textual->_textwidth += mx_platform_font_width(ffont, " ", -1);

	mx__edittext_cursorpos(edit, edit->_cursor);
}

static void mx__edittext_setter(MX_TEXTUAL_DATA * textual, const char *str, int len, MX_FREE dfree)
{
	MX_EDITTEXT_DATA *edit = (MX_EDITTEXT_DATA *) textual;

	MXDEBUG_INVARIANT(MXOBJ(textual));

	if ((str) && (len == -1))
		len = strlen(str);

	/* The edittext vector takes over the buffer */
	mx_vector_resize(&edit->_text, 0);
	if ((str) && (len > 0))
		mx_vector_append(&edit->_text, str, len);

	/* The text string must point to the vector buffer */
	mx_string_set(&textual->_text, edit->_text.data, len, 0);

	/* We have allocated our own copy, so remove users copy */
	if ((str) && (dfree))
		dfree((void *) str);

	mx__edittext_chunker(textual);
}

static const char *mx__edittext_getter(const MX_TEXTUAL_DATA * textual, int *len)
{
	MX_EDITTEXT_DATA *edit = (MX_EDITTEXT_DATA *) textual;

	MXDEBUG_INVARIANT(MXOBJ(textual));

	if (len)
		*len = mx_vector_size(&edit->_text);

	return edit->_text.data;
}

void mx__edittext_zeroterminate(MX_EDITTEXT_DATA * edit)
{
	MXDEBUG_INVARIANT(MXOBJ(edit));

	/* This function could theoretically cause the vector to reallocate and
	   require texutal rechunking because text pointers wont be correct
	   anymore. But wherever we use the vector we reserve some extra room for
	   the zero terminator so the appending shouldnt damage the chunk
	   validity. */
	mx_vector_append1(&edit->_text, 0);
	mx_vector_remove_last(&edit->_text);
}

void mx__edittext_cursorscroll(MX_EDITTEXT_DATA * edit)
{
	if (edit->_cursorchunk) {
		MX_RECT rrect;

		rrect = edit->_cursorchunk->rect;
		mx_emit(MX_SCROLLAREA, &rrect, sizeof(rrect));
	}
}

void mx__edittext_insert(MX_EDITTEXT_DATA * edit, const char *text, const unsigned int num)
{
	MX_TEXTUAL_DATA *textual = MXTEXTUAL(edit);

	MXDEBUG_INVARIANT(MXOBJ(edit));

	if (edit->_cursor < 0)
		return;

	mx_vector_insert(&edit->_text, text, num, edit->_cursor);
	mx_vector_reserve(&edit->_text, mx_vector_size(&edit->_text) + 10);

	edit->_cursor += num;
	mx__textual_chunk(textual);
}

void mx__edittext_backspace(MX_EDITTEXT_DATA * edit)
{
	const unsigned int presize = mx_vector_size(&edit->_text);

	MX_TEXTUAL_DATA *textual = MXTEXTUAL(edit);

	MXDEBUG_INVARIANT(MXOBJ(edit));

	if (edit->_cursor < 0)
		return;

	mx_vector_remove(&edit->_text, edit->_cursor - 1, 1);

	if (presize != mx_vector_size(&edit->_text)) {
		--edit->_cursor;
		mx__textual_chunk(textual);
	}
}

void mx__edittext_delete(MX_EDITTEXT_DATA * edit)
{
	MX_TEXTUAL_DATA *textual = MXTEXTUAL(edit);
	const unsigned int presize = mx_vector_size(&edit->_text);

	MXDEBUG_INVARIANT(MXOBJ(edit));

	if ((edit->_cursor < 0) || (presize == 0))
		return;

	mx_vector_remove(&edit->_text, edit->_cursor, 1);

	if (presize != mx_vector_size(&edit->_text))
		mx__textual_chunk(textual);
}

static void mx__edittext_cursorpixel(MX_EDITTEXT_DATA * edit, const MX__TEXTCHUNK * chunk, const int x)
{
	int n = 0, len = 0;
	const char *text = mx_string_text(&chunk->text, &len);
	void *ffont = mx__textual_platform_font(MXTEXTUAL(edit));
	int pixeldiff = x - chunk->rect.x1;
	int lastdiff = pixeldiff;

	while ((n < len) && (pixeldiff > 0)) {
		const int clen = mx_utf8_len(text);

		lastdiff = pixeldiff;
		pixeldiff -= mx_platform_font_width(ffont, text, clen);
		n += clen;
		text += clen;
	}

	if ((n) && (abs(lastdiff) < abs(pixeldiff)))
		--n;

	mx__edittext_cursorpos(edit, chunk->offset + n);
}

void mx__edittext_up(MX_EDITTEXT_DATA * edit)
{
	int x, y;
	MX__TEXTCHUNK *chunk, *ptr;

	MXDEBUG_INVARIANT(MXOBJ(edit));

	if (edit->_cursor < 0)
		return;
	if (!edit->_cursorpointedchunk)
		mx__edittext_cursorpos(edit, edit->_cursor);
	assert(edit->_cursorpointedchunk);

	x = edit->_pixeloffset;
	y = edit->_cursorpointedchunk->rect.y2;

	chunk = mx_dllist_prev(edit->_cursorpointedchunk);

	/* Go one line higher */
	while (chunk) {
		if ((chunk->rect.y2 != y) && (chunk->normal))
			break;
		chunk = mx_dllist_prev(chunk);
	}
	if (chunk)
		y = chunk->rect.y2;

	/* Look on the same line for a good chunk */
	ptr = chunk;
	while (ptr) {
		if ((ptr->normal) && (ptr->rect.y2 == y)) {
			chunk = ptr;
			if (ptr->rect.x1 < x)
				break;
		}
		ptr = mx_dllist_prev(ptr);
	}

	if (chunk) {
		assert(chunk->normal);
		mx__edittext_cursorpixel(edit, chunk, x);
	}

	edit->_pixeloffset = x;
}

void mx__edittext_down(MX_EDITTEXT_DATA * edit)
{
	int x, y;
	MX__TEXTCHUNK *chunk, *ptr;

	MXDEBUG_INVARIANT(MXOBJ(edit));

	if (edit->_cursor < 0)
		return;
	if (!edit->_cursorpointedchunk)
		mx__edittext_cursorpos(edit, edit->_cursor);
	assert(edit->_cursorpointedchunk);

	x = edit->_pixeloffset;
	y = edit->_cursorpointedchunk->rect.y2;

	chunk = mx_dllist_next(edit->_cursorpointedchunk);

	/* Go one line lower */
	while (chunk) {
		if ((chunk->rect.y2 != y) && (chunk->normal))
			break;
		chunk = mx_dllist_next(chunk);
	}
	if (chunk)
		y = chunk->rect.y2;

	/* Look on the same line for a good chunk */
	ptr = chunk;
	while (ptr) {
		if ((ptr->normal) && (ptr->rect.y2 == y)) {
			chunk = ptr;
			if (ptr->rect.x2 >= x)
				break;
		}
		ptr = mx_dllist_next(ptr);
	}

	if (chunk) {
		assert(chunk->normal);
		mx__edittext_cursorpixel(edit, chunk, x);
	}

	edit->_pixeloffset = x;
}

void mx__edittext_left(MX_EDITTEXT_DATA * edit)
{
	MXDEBUG_INVARIANT(MXOBJ(edit));

	if (edit->_cursor < 1)
		return;

	mx__edittext_cursorpos(edit, edit->_cursor - 1);
}

void mx__edittext_right(MX_EDITTEXT_DATA * edit)
{
	MXDEBUG_INVARIANT(MXOBJ(edit));

	if (edit->_cursor < 0)
		return;

	mx__edittext_cursorpos(edit, edit->_cursor + 1);
}

void mx__edittext_home(MX_EDITTEXT_DATA * edit)
{
	int y;
	MX__TEXTCHUNK *chunk, *ptr;

	MXDEBUG_INVARIANT(MXOBJ(edit));

	if (edit->_cursor < 0)
		return;
	if (!edit->_cursorpointedchunk)
		mx__edittext_cursorpos(edit, edit->_cursor);
	assert(edit->_cursorpointedchunk);

	y = edit->_cursorpointedchunk->rect.y2;

	chunk = edit->_cursorpointedchunk;
	ptr = mx_dllist_prev(chunk);

	while (ptr) {
		if ((ptr->rect.y2 == y) && (ptr->normal))
			chunk = ptr;
		ptr = mx_dllist_prev(ptr);
	}

	if (chunk)
		mx__edittext_cursorpos(edit, chunk->offset);
}

void mx__edittext_end(MX_EDITTEXT_DATA * edit)
{
	int y;
	MX__TEXTCHUNK *chunk, *ptr;

	MXDEBUG_INVARIANT(MXOBJ(edit));

	if (edit->_cursor < 0)
		return;
	if (!edit->_cursorpointedchunk)
		mx__edittext_cursorpos(edit, edit->_cursor);
	assert(edit->_cursorpointedchunk);

	y = edit->_cursorpointedchunk->rect.y2;

	chunk = edit->_cursorpointedchunk;
	ptr = mx_dllist_next(chunk);

	while (ptr) {
		if ((ptr->rect.y2 == y) && (ptr->normal))
			chunk = ptr;
		ptr = mx_dllist_next(ptr);
	}

	if (chunk) {
		const int len = mx_string_len(&chunk->text);

		mx__edittext_cursorpos(edit, chunk->offset + len);
	}
}

static void mx__edittext_pointerpressed(MX_EDITTEXT_DATA * edit, int x, int y)
{
	int xdiff = -1;
	MX__TEXTCHUNK *chunk, *ptr;

	MXDEBUG_INVARIANT(MXOBJ(edit));

	if (edit->_cursor < 0)
		return;

	x -= mx_x1(edit);
	y -= mx_y1(edit);

	chunk = 0;
	ptr = mx_dllist_first(MXTEXTUAL(edit));

	while (ptr) {
		if ((ptr->normal) && (ptr->rect.y1 <= y)
			&& (ptr->rect.y2 >= y)) {
			const int xd = MX_MIN(x - ptr->rect.x1, ptr->rect.x2 - x);

			if ((xdiff == -1) || (xd > xdiff)) {
				chunk = ptr;
				xdiff = xd;
			}
		}
		ptr = mx_dllist_next(ptr);
	}

	if (chunk)
		mx__edittext_cursorpixel(edit, chunk, x);
}

void mx_edittext_class(void)
{
	MX_EDITTEXT *e = (MX_EDITTEXT *) mx.obj;
	MX_EDITTEXT_DATA *edit = MXEDITTEXT(e);
	MX_TEXTUAL_DATA *textual = MXTEXTUAL(edit);

	MXDEBUG_CLASSINVARIANT(MXOBJ(e));

	switch (mx.event) {

	case MX_DESTRUCT:
		edit->_cursorchunk = 0;
		edit->_cursorpointedchunk = 0;
		mx_vector_free(&edit->_text);
		break;

	case MX_EXPOSE:
		mx__theme->textual((MX_TEXTUAL *) textual);
		return;

	case MX_POINTER_PRESS:
		{
			const MX_POINTER_INFO *info = mx_pointer_info();

			mx__edittext_pointerpressed(edit, info->x, info->y);
			return;
		}

	case MX_KEY:
		{
			/* We assume we use the key */
			unsigned usedkey = true;
			const MX_KEY_INFO *info = mx_key_info();

			assert(info);

			/* I think we should standardize on '\n' as the return character */
			if (info->ascii == '\r') {
				mx__edittext_insert(edit, "\n", 1);

				/* Standard ascii keys means insert into the editable text */
			} else if (((info->ascii > 20) && (info->ascii < 128))
					   || (info->ascii == '\r')) {
				char text[3];

				text[0] = info->ascii;
				text[1] = 0;

				mx__edittext_insert(edit, text, 1);

				/* Handle tab */
			} else if (info->ascii == 9) {

				mx__edittext_insert(edit, "\t", 1);

				/* Handle backspace */
			} else if (info->ascii == 8) {

				mx__edittext_backspace(edit);

				/**todo Add handling of other non-printing ascii keys */

				/* Check for control keys */
			} else if (info->ascii == 0) {

				switch (info->code) {

					/* Delete key */
				case 83:
					mx__edittext_delete(edit);
					break;

					/* Cursor home */
				case 71:
					mx__edittext_home(edit);
					break;

					/* Cursor end */
				case 79:
					mx__edittext_end(edit);
					break;

					/* Cursor up */
				case 72:
					mx__edittext_up(edit);
					break;

					/* Cursor down */
				case 80:
					mx__edittext_down(edit);
					break;

					/* Cursor left */
				case 75:
					mx__edittext_left(edit);
					break;

					/* Cursor right */
				case 77:
					mx__edittext_right(edit);
					break;

					/**todo Add handling of pageup/pagedown keys */

					/* We didnt use the control key */
				default:
					usedkey = false;
					break;
				}
				/* We didnt use the ascii key */
			} else
				usedkey = false;

			/* Update if we used the key */
			if (usedkey)
				mx_dirty(edit, true);

			mx_answer(usedkey);
			return;
		}

	default:
		break;
	}

	mx_textual_class();
}

MX_EDITTEXT *mx__edittext(MX_EDITTEXT_DATA * edit, size_t size, MX_OBJ_DATA * parent, int theid)
{
	MXMINSIZE(size, MX_EDITTEXT);

	edit = (MX_EDITTEXT_DATA *) mx__textual(MXTEXTUAL(edit), size, parent, theid);
	if (edit) {
		MXOBJ(edit)->_class = mx_edittext_class;
		MXDEBUG_ALLOCTAG(edit);
		MXDEBUG_ATOMNAME(edit, "edittext");

		MXTEXTUAL(edit)->_getter = mx__edittext_getter;
		MXTEXTUAL(edit)->_setter = mx__edittext_setter;
		MXTEXTUAL(edit)->_chunker = mx__edittext_chunker;
		edit->_cursor = 0;

		mx_vector(&edit->_text);
		mx_wantfocus(edit, true);
		mx_wantpointer(edit, true);
	}

	MXDEBUG_INVARIANT(MXOBJ(edit));
	return (MX_EDITTEXT *) edit;
}
